Skip to content

Bound every action-log read path - #298

Merged
ndisidore merged 5 commits into
mainfrom
chore/scale-action-logs
Aug 25, 2026
Merged

Bound every action-log read path#298
ndisidore merged 5 commits into
mainfrom
chore/scale-action-logs

Conversation

@ndisidore

@ndisidore ndisidore commented Aug 21, 2026

Copy link
Copy Markdown
Member

Reading the action log today means reading all of it: opening a workspace replays every record ever written to each client, the Activity pane holds the whole log in React state, and the auto-approval drain materializes the full table per run. Long-lived workspaces pay for this on every open, and the cost only grows.

This branch bounds every read path and splits the protocol into query-for-state / subscribe-for-deltas:

  • subscribeToActions delivers live updates only. Clients fetch the current pending set through listActions({filter: "pending"}) after initiating the subscribe; capnweb e-order makes the pair gapless. The unpaced replay push is gone; initial state flows over pull-paged, client-clocked reads.
  • The new listActions RPC pages history newest-first: resolved records under a raw-scan cap, so a log buried in resolved records returns short pages with a cursor instead of stalling the DO, and pending records off a new sparse pending-by-gatekeeper index
  • The Activity pane demand-loads history one page at a time (type filters, "Load older", failure/retry states). Pending state runs off one ref-counted store shared per overseer stub: it subscribes, then pages the pending filter
  • Chat action cards reconcile on reconnect: since the live-only stream never redelivers a resolution missed while disconnected, cached cards still shown pending are re-fetched, a few at a time.
  • AutoApprovalDrainer reads its gatekeeper's pendings off the index instead of scanning the log.
  • Pre-deploy clients that still pass startAfter get a paced full replay

Tested with unit suites for the subscription, pagination, migration-backfill, and drain paths, an integration smoke over the paged RPCs, and hook tests for the new frontend state.

@github-actions github-actions Bot added workshop/frontend Changes to the Workshop frontend kernel Changes to the Workshop kernel workshop/shared Changes to shared Workshop APIs labels Aug 21, 2026
@ndisidore
ndisidore force-pushed the chore/scale-action-logs branch from 7a6dcf3 to eef947d Compare August 21, 2026 19:51
@github-actions github-actions Bot added the gatekeeper Changes to a gatekeeper integration label Aug 21, 2026
@ndisidore
ndisidore force-pushed the chore/scale-action-logs branch from 03c86bd to eef947d Compare August 21, 2026 20:12
@github-actions github-actions Bot removed the gatekeeper Changes to a gatekeeper integration label Aug 21, 2026
Comment thread packages/workshop-shared/src/api.ts Outdated
@ndisidore
ndisidore force-pushed the chore/scale-action-logs branch from eef947d to dee1f52 Compare August 24, 2026 20:32
@github-actions

Copy link
Copy Markdown

Preview: pr298-chore-scale-a-eee7eec7

https://pr298-chore-scale-a-eee7eec7-router.cloudflare-os-previews.workers.dev

Dashboard · deleted when this PR closes

Comment thread packages/workshop-shared/src/api.ts Outdated
Comment thread packages/workshop-shared/src/api.ts Outdated
Comment thread packages/workshop-shared/src/api.ts Outdated
Comment thread packages/workshop-shared/src/api.ts Outdated
Comment thread packages/workshop-shared/src/api.ts Outdated
Comment thread packages/workshop-backend/src/overseer.ts Outdated
Comment thread packages/workshop-backend/src/overseer.ts
Comment thread packages/workshop-backend/src/overseer.ts
Comment thread packages/workshop-backend/src/overseer.ts Outdated
Comment thread packages/workshop-backend/src/overseer.ts Outdated
@ndisidore
ndisidore force-pushed the chore/scale-action-logs branch from cc8359a to cee048e Compare August 25, 2026 14:18
@ndisidore
ndisidore force-pushed the chore/scale-action-logs branch from cee048e to bfe5841 Compare August 25, 2026 15:14
@ndisidore
ndisidore force-pushed the chore/scale-action-logs branch from bfe5841 to 13726ee Compare August 25, 2026 17:06
Comment thread packages/typed-storage/src/index.ts Outdated
Comment thread packages/workshop-backend/src/overseer.ts Outdated
Comment thread packages/workshop-backend/src/overseer.ts Outdated
Comment thread packages/workshop-backend/src/overseer.ts Outdated
Comment thread packages/workshop-backend/src/overseer.ts Outdated
Review round 3: replace the id-ordered resume sweep (still O(log) per
resubscribe) with a byLastChanged unique index -- last state-change time,
id-disambiguated for the frozen clock -- so startAfter replays only the
records changed during the gap, inclusively (an exclusive bound loses
same-instant siblings). Merge the two index migrations into one v2 -> 3
step covering all three action indexes, drop durationMs from its log
(always zero in sync code), drop byHistoryFilter's redundant "all" bucket
(the collection itself already serves it in id order), and trim comments
per review.
ndisidore added a commit that referenced this pull request Aug 25, 2026
Drop the pending-set carryover: a settled store now parks just its last
change time by workspace key, and the next linked store opens exactly as
a cold one — subscribe, then page pending — passing the watermark as
startAfter. The gap still replays as upserts through the subscription
(which is what useActionHistory and ChatInterface rely on), the fresh
pages re-snapshot the pending set, and the page loop stays the single
settled signal. Buys back the seeding/skip-page dual path for the cost
of re-paging a set PR #298 already made cheap.
@ndisidore
ndisidore merged commit d56a004 into main Aug 25, 2026
12 of 14 checks passed
@ndisidore
ndisidore deleted the chore/scale-action-logs branch August 25, 2026 20:36
ndisidore added a commit that referenced this pull request Aug 25, 2026
Drop the pending-set carryover: a settled store now parks just its last
change time by workspace key, and the next linked store opens exactly as
a cold one — subscribe, then page pending — passing the watermark as
startAfter. The gap still replays as upserts through the subscription
(which is what useActionHistory and ChatInterface rely on), the fresh
pages re-snapshot the pending set, and the page loop stays the single
settled signal. Buys back the seeding/skip-page dual path for the cost
of re-paging a set PR #298 already made cheap.
ndisidore added a commit that referenced this pull request Aug 25, 2026
Drop the pending-set carryover: a settled store now parks just its last
change time by workspace key, and the next linked store opens exactly as
a cold one — subscribe, then page pending — passing the watermark as
startAfter. The gap still replays as upserts through the subscription
(which is what useActionHistory and ChatInterface rely on), the fresh
pages re-snapshot the pending set, and the page loop stays the single
settled signal. Buys back the seeding/skip-page dual path for the cost
of re-paging a set PR #298 already made cheap.
ndisidore added a commit that referenced this pull request Aug 25, 2026
* feat(frontend): resume action subscriptions with startAfter on reconnect

A settled shared action store now parks its pending set and change-time
watermark (max appliedAt ?? createdAt received) by workspace key when it
closes. The next store linked to the same key seeds from that carryover
and subscribes with startAfter, so the server replays only the gap as
upserts instead of the store re-paging the whole pending set — the
subscribe call resolving is the settled signal. Unsettled or errored
sessions never seed a resume, and unlinked stubs keep cold-open behavior.

useWorkspaceOpen links every minted Overseer stub to its workspace id.
useActionHistory keeps its loaded window and cursor across a resumed
swap (dropping in-flight old-stub pages), and ChatInterface's gap-refetch
effect is now only the cold-open safety net, resolving its startAfter TODO.

* refactor(frontend): carry only a resume watermark across reconnects

Drop the pending-set carryover: a settled store now parks just its last
change time by workspace key, and the next linked store opens exactly as
a cold one — subscribe, then page pending — passing the watermark as
startAfter. The gap still replays as upserts through the subscription
(which is what useActionHistory and ChatInterface rely on), the fresh
pages re-snapshot the pending set, and the page loop stays the single
settled signal. Buys back the seeding/skip-page dual path for the cost
of re-paging a set PR #298 already made cheap.

* fix(frontend): recover from failed action log resume

* fix(frontend): harden the resume watermark and repair channel

Review fixes for the reconnect-resume stack:

A store now parks its watermark only after a cleanly settled session:
pages drained AND the subscribe call resolved (the server delivers the
gap replay before resolving, so 'ready' alone can predate undelivered
replay records) AND no entry listener threw on a delivery. ChatInterface
carries cards a refetch run failed to repair (error, or cancelled
mid-loop) to the next reconnect and retries them even when the resume
replay covers everything else — restoring the repair channel the resume
skip removed.

Also: actionChangeTime() in workshop-shared now owns the
appliedAt ?? createdAt formula shared by the server's byLastChanged
index key, the client watermark, and Activity's display; a status-only
useActionStatus() spares ChatInterface/useActionHistory a re-render per
pending-set change; useActionHistory dedupes its session resets; the
ChatInterface action test uses the shared harness root (fixing a leaked
rAF queue) — plus regression tests for the three behavior changes.

* refactor(frontend): drop the resume failure fallbacks

The consumer-side recovery shell (useActionStatus, resumeFallbackRequired,
unrepairedCardsRef, entryListenerFailed) defended a resumed subscription
failing while the stub stays healthy — reachable only through a server-side
bug, since transport failures swap the stub. The store's invariant already
guarantees eventual healing without it: a failed session never parks a
watermark, so the next stub swap replays its entire gap from the last good
one. Until then the store shows status 'error', so nothing degrades silently.

entryListenerFailed additionally protected nothing real: the refetch path
applies updates through the same applyActionLogUpdateToCachedMessages the
listener uses, so a record that throws in one path throws in both.

Kept the subscription-resolved watermark gate — a page-only watermark is
poison (a pending record's createdAt can exceed a missed resolution's
appliedAt, hiding it from every future replay) — and documented on
actionLogResumed why consumers may trust it without a failure path.
darjss pushed a commit to darjss/cloudflare-os-erxes that referenced this pull request Aug 26, 2026
* feat(typed-storage): support ranged reads and index rebuilds

* feat(shared+backend): add indexed paginated action history

* fix(backend): bound action replay and auto-approval scans

* feat(frontend): page action history and reconcile live actions

* fix(backend): index the resume replay by last change time

Review round 3: replace the id-ordered resume sweep (still O(log) per
resubscribe) with a byLastChanged unique index -- last state-change time,
id-disambiguated for the frozen clock -- so startAfter replays only the
records changed during the gap, inclusively (an exclusive bound loses
same-instant siblings). Merge the two index migrations into one v2 -> 3
step covering all three action indexes, drop durationMs from its log
(always zero in sync code), drop byHistoryFilter's redundant "all" bucket
(the collection itself already serves it in id order), and trim comments
per review.
darjss pushed a commit to darjss/cloudflare-os-erxes that referenced this pull request Aug 26, 2026
* feat(frontend): resume action subscriptions with startAfter on reconnect

A settled shared action store now parks its pending set and change-time
watermark (max appliedAt ?? createdAt received) by workspace key when it
closes. The next store linked to the same key seeds from that carryover
and subscribes with startAfter, so the server replays only the gap as
upserts instead of the store re-paging the whole pending set — the
subscribe call resolving is the settled signal. Unsettled or errored
sessions never seed a resume, and unlinked stubs keep cold-open behavior.

useWorkspaceOpen links every minted Overseer stub to its workspace id.
useActionHistory keeps its loaded window and cursor across a resumed
swap (dropping in-flight old-stub pages), and ChatInterface's gap-refetch
effect is now only the cold-open safety net, resolving its startAfter TODO.

* refactor(frontend): carry only a resume watermark across reconnects

Drop the pending-set carryover: a settled store now parks just its last
change time by workspace key, and the next linked store opens exactly as
a cold one — subscribe, then page pending — passing the watermark as
startAfter. The gap still replays as upserts through the subscription
(which is what useActionHistory and ChatInterface rely on), the fresh
pages re-snapshot the pending set, and the page loop stays the single
settled signal. Buys back the seeding/skip-page dual path for the cost
of re-paging a set PR cloudflare#298 already made cheap.

* fix(frontend): recover from failed action log resume

* fix(frontend): harden the resume watermark and repair channel

Review fixes for the reconnect-resume stack:

A store now parks its watermark only after a cleanly settled session:
pages drained AND the subscribe call resolved (the server delivers the
gap replay before resolving, so 'ready' alone can predate undelivered
replay records) AND no entry listener threw on a delivery. ChatInterface
carries cards a refetch run failed to repair (error, or cancelled
mid-loop) to the next reconnect and retries them even when the resume
replay covers everything else — restoring the repair channel the resume
skip removed.

Also: actionChangeTime() in workshop-shared now owns the
appliedAt ?? createdAt formula shared by the server's byLastChanged
index key, the client watermark, and Activity's display; a status-only
useActionStatus() spares ChatInterface/useActionHistory a re-render per
pending-set change; useActionHistory dedupes its session resets; the
ChatInterface action test uses the shared harness root (fixing a leaked
rAF queue) — plus regression tests for the three behavior changes.

* refactor(frontend): drop the resume failure fallbacks

The consumer-side recovery shell (useActionStatus, resumeFallbackRequired,
unrepairedCardsRef, entryListenerFailed) defended a resumed subscription
failing while the stub stays healthy — reachable only through a server-side
bug, since transport failures swap the stub. The store's invariant already
guarantees eventual healing without it: a failed session never parks a
watermark, so the next stub swap replays its entire gap from the last good
one. Until then the store shows status 'error', so nothing degrades silently.

entryListenerFailed additionally protected nothing real: the refetch path
applies updates through the same applyActionLogUpdateToCachedMessages the
listener uses, so a record that throws in one path throws in both.

Kept the subscription-resolved watermark gate — a page-only watermark is
poison (a pending record's createdAt can exceed a missed resolution's
appliedAt, hiding it from every future replay) — and documented on
actionLogResumed why consumers may trust it without a failure path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kernel Changes to the Workshop kernel workshop/frontend Changes to the Workshop frontend workshop/shared Changes to shared Workshop APIs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants